'use client'; import './style.scss'; import Link from 'next/link'; import Image from 'next/image'; import { useRouter } from 'next/navigation'; import { useState, useEffect, useCallback, useMemo, MouseEvent } from 'react'; import useErrorAlert from '@/hooks/useErrorAlert'; import { Menu } from 'lucide-react'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faBookmark as nBookmark, faThumbsUp as nThumbsUp, faThumbsDown as nThumbsDown, faFlag as nFlag, faPenToSquare, faTrashCan } from '@fortawesome/free-regular-svg-icons'; import { faQrcode, faPrint, faLink, faShareNodes, faBookmark as yBookmark, faThumbsUp as yThumbsUp, faThumbsDown as yThumbsDown, faFlag as yFlag } from '@fortawesome/free-solid-svg-icons'; import Loading from '@/app/component/Loading'; import Comment from '@/app/(main)/(forum)/comment/view'; import LatestList from '@/app/(main)/(forum)/post/_component/LatestPosts'; import { BoardResponse } from '@/types/response/forum/board'; import { PostResponse } from '@/types/response/forum/post'; import Content from '../_component/Content'; import QRCode from '../_component/QRCode'; import Copied from '../_component/Copied'; import SnsShare from '../_component/SnsShare'; import Report from '../_component/Report'; import { Reaction } from '@/constants/forum'; import { fetchApi, getDateTime, formatDate, isDateOverdue } from '@/lib/utils/client'; import { isBoardAdmin } from '@/lib/utils/permission'; import useAuth from '@/hooks/useAuth'; type Props = { _board: BoardResponse, _post: PostResponse }; export default function View({ _board, _post }: Props) { const router = useRouter(); const { member, loginCheck } = useAuth(); // 신고 횟수 초과 게시글은 접근 불가 (첫 진입 시 1회만 검사) useEffect(() => { if (_post.reports > _board.boardMeta.view.blameHideCount && _board.boardMeta.view.blameHideCount > 0) { alert('비공개 게시글입니다.'); router.replace(`/board/${_post.boardCode}${window.location.search}`); } }, [_post.reports, _post.boardCode, _board.boardMeta.view.blameHideCount, router]); const { setError } = useErrorAlert(); const [loading, setLoading] = useState(false); const [qrCode, setQrCode] = useState(false); const [copied, setCopied] = useState(false); const [snsShare, setSnsShare] = useState(false); const [report, setReport] = useState(false); const [hasLike, setHasLike] = useState(_post.hasLike); const [hasDisLike, setHasDisLike] = useState(_post.hasDislike); const [hasBookmark, setHasBookmark] = useState(_post.hasBookmark); const [hasReport, setHasReport] = useState(_post.hasReport); const isOwner = member?.id === _post.memberID; const canManage = isOwner || isBoardAdmin(_board.boardManager, member); const writerThumb = useMemo(() => _post.writer?.thumb ?? '/resources/thumb.gif', [_post.writer?.thumb]); const writerIcon = useMemo(() => _post.writer?.icon ?? _post.writer?.gradeImage ?? null, [_post.writer?.icon, _post.writer?.gradeImage]); const writerName = useMemo(() => _post.writer?.name ?? _post.name ?? '-', [_post.writer?.name, _post.name]); const hasFunction = _board.boardMeta.view.allowLike || _board.boardMeta.view.allowDislike || _board.boardMeta.view.allowBookmark || _board.boardMeta.view.allowBlame || canManage; const toggleQRCode = useCallback((e: MouseEvent) => { e.preventDefault(); setQrCode((prev) => !prev); }, []); const handlePrint = useCallback(() => { window.print(); }, []); const toggleCopied = useCallback((e: MouseEvent) => { e.preventDefault(); setCopied((prev) => !prev); }, []); const toggleSnsShare = useCallback((e: MouseEvent) => { e.preventDefault(); setSnsShare((prev) => !prev); }, []); // 좋아요/싫어요 const handleReaction = useCallback(async (e: MouseEvent) => { const reaction = Number(e.currentTarget.value); if (!loginCheck()) { return; } setLoading(true); try { const res = await fetchApi('/api/forum/posts/' + _post.id + '/reaction', { method: 'POST', body: { reaction } }); if (res.success) { switch (reaction) { case Reaction.Like: setHasLike(prev => !prev); setHasDisLike(false); break; case Reaction.Dislike: setHasDisLike(prev => !prev); setHasLike(false); break; } } } catch (err) { if (err instanceof Error) setError(err.message); } finally { setLoading(false); } }, [_post.id, loginCheck, setError]); // 즐겨찾기 const handleBookmark = useCallback(async () => { if (!loginCheck()) { return; } setLoading(true); try { const res = await fetchApi('/api/forum/posts/' + _post.id + '/bookmark', { method: 'POST' }); if (res.success) { setHasBookmark(prev => !prev); } } catch (err) { if (err instanceof Error) setError(err.message); } finally { setLoading(false); } }, [_post.id, loginCheck, setError]); // 신고하기 시작 const handleReport = useCallback(() => { if (hasReport) { alert('이미 신고하셨습니다.'); return; } if (!loginCheck()) { return; } setReport((prev) => !prev); }, [loginCheck, hasReport]); // 수정하기 const handleEdit = useCallback(() => { if (!loginCheck()) { return; } // 게시글 삭제 보호 확인 if (_board.boardMeta.general.allowUpdateProtection && !isBoardAdmin(_board.boardManager, member)) { if (isDateOverdue(_post.createdAt, _board.boardMeta.general.updateProtectionDays)) { return alert(`게시글 작성 후 ${_board.boardMeta.general.updateProtectionDays}일이 지나 수정이 불가능합니다.`); } } router.push(`/post/edit/${_post.id}`); }, [loginCheck, member, _board, _post, router]); // 게시글 삭제 const handleDelete = useCallback(() => { if (!loginCheck()) { return; } // 게시글 삭제 보호 확인 if (_board.boardMeta.general.allowDeleteProtection && !isBoardAdmin(_board.boardManager, member)) { if (isDateOverdue(_post.createdAt, _board.boardMeta.general.deleteProtectionDays)) { return alert(`게시글 작성 후 ${_board.boardMeta.general.deleteProtectionDays}일이 지나 삭제가 불가능합니다.`); } } if (confirm('정말 삭제하시겠습니까?')) { setLoading(true); fetchApi('/api/forum/posts/' + _post.id, { method: 'DELETE' }).then(res => { if (res.success) { alert('게시글이 삭제되었습니다.'); router.push(`/board/${_post.boardCode}`); } }).catch((err) => { setError(err.message); }).finally(() => { setLoading(false); }); } }, [loginCheck, member, _board, _post, router, setError]); return (
{loading && } {/* 글 제목 */}
{_post.boardPrefixID && ("[" + _post.boardPrefix.name + "]")} {_post.subject}

{/* 글 작성자/작성일시/부가기능들 */}
{_board.boardMeta.view.showMemberThumb && (
회원 사진
)}
    {_post.writer ? ( <>
  • {_board.boardMeta.view.showMemberIcon && writerIcon && ( )} {writerName}
  • {_board.boardMeta.view.showMemberRegDate && (
  • {formatDate(_post.writer.createdAt)} 가입
  • )} {_board.boardMeta.view.showMemberSummary && (
  • {_post.writer.summary}
  • )} ) : (
  • {_post.name ?? '-'}
  • )}
  • 조회 {_post.views}
  • 댓글 {_post.comments}
  • {_board.boardMeta.view.allowLike && (
  • 좋아요 {_post.likes}
  • )} {_board.boardMeta.view.allowDislike && (
  • 싫어요 {_post.dislikes}
  • )}
  • IP {_post.ipAddress}
작성일시 {getDateTime(_post.createdAt)}
    {_board.boardMeta.view.allowPostUrlQrCode && (
  • QR
  • )} {_board.boardMeta.view.allowPrint && (
  • 인쇄
  • )} {_board.boardMeta.view.allowPostUrlCopy && (
  • 주소
  • )} {_board.boardMeta.view.allowSnsShare && (
  • 공유
  • )}

{/* 글 내용 */}
{_post.tagList.length > 0 && (
{/* 태그 표시 */} {_post.tagList.map((row, i) => ( #{row.slug} ))}
)}

{/* 제어 버튼들 */}
목록 {hasFunction && _board.boardMeta.view.allowPrevNextBotton && ( <> {!!_post.prevID && ( 이전 )} {!!_post.nextID && ( 다음 )} )}
{hasFunction ? (
{_board.boardMeta.view.allowLike && (
)} {_board.boardMeta.view.allowDislike && (
)} {_board.boardMeta.view.allowBookmark && (
)} {_board.boardMeta.view.allowBlame && (
)} {canManage && ( <>
)}
{_board.boardMeta.view.allowLike && ( )} {_board.boardMeta.view.allowDislike && ( )} {_board.boardMeta.view.allowBookmark && ( 즐겨찾기 )} {_board.boardMeta.view.allowBlame && ( 신고 )} {canManage && ( <> 수정 삭제 )}
) : ( _board.boardMeta.view.allowPrevNextBotton && (
{!!_post.prevID && ( 이전 )} {!!_post.nextID && ( 다음 )}
) )}

{/* 댓글 */} {/* 게시판 최근 글 */}
); }